// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "net/proxy/proxy_config.h"
#include "net/proxy/proxy_config_service_common_unittest.h"
#include "net/proxy/proxy_info.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace net {
namespace {

    void ExpectProxyServerEquals(const char* expectation,
        const ProxyList& proxy_servers)
    {
        if (expectation == NULL) {
            EXPECT_TRUE(proxy_servers.IsEmpty());
        } else {
            EXPECT_EQ(expectation, proxy_servers.ToPacString());
        }
    }

    TEST(ProxyConfigTest, Equals)
    {
        // Test |ProxyConfig::auto_detect|.

        ProxyConfig config1;
        config1.set_auto_detect(true);

        ProxyConfig config2;
        config2.set_auto_detect(false);

        EXPECT_FALSE(config1.Equals(config2));
        EXPECT_FALSE(config2.Equals(config1));

        config2.set_auto_detect(true);

        EXPECT_TRUE(config1.Equals(config2));
        EXPECT_TRUE(config2.Equals(config1));

        // Test |ProxyConfig::pac_url|.

        config2.set_pac_url(GURL("http://wpad/wpad.dat"));

        EXPECT_FALSE(config1.Equals(config2));
        EXPECT_FALSE(config2.Equals(config1));

        config1.set_pac_url(GURL("http://wpad/wpad.dat"));

        EXPECT_TRUE(config1.Equals(config2));
        EXPECT_TRUE(config2.Equals(config1));

        // Test |ProxyConfig::proxy_rules|.

        config2.proxy_rules().type = ProxyConfig::ProxyRules::TYPE_SINGLE_PROXY;
        config2.proxy_rules().single_proxies.SetSingleProxyServer(
            ProxyServer::FromURI("myproxy:80", ProxyServer::SCHEME_HTTP));

        EXPECT_FALSE(config1.Equals(config2));
        EXPECT_FALSE(config2.Equals(config1));

        config1.proxy_rules().type = ProxyConfig::ProxyRules::TYPE_SINGLE_PROXY;
        config1.proxy_rules().single_proxies.SetSingleProxyServer(
            ProxyServer::FromURI("myproxy:100", ProxyServer::SCHEME_HTTP));

        EXPECT_FALSE(config1.Equals(config2));
        EXPECT_FALSE(config2.Equals(config1));

        config1.proxy_rules().single_proxies.SetSingleProxyServer(
            ProxyServer::FromURI("myproxy", ProxyServer::SCHEME_HTTP));

        EXPECT_TRUE(config1.Equals(config2));
        EXPECT_TRUE(config2.Equals(config1));

        // Test |ProxyConfig::bypass_rules|.

        config2.proxy_rules().bypass_rules.AddRuleFromString("*.google.com");

        EXPECT_FALSE(config1.Equals(config2));
        EXPECT_FALSE(config2.Equals(config1));

        config1.proxy_rules().bypass_rules.AddRuleFromString("*.google.com");

        EXPECT_TRUE(config1.Equals(config2));
        EXPECT_TRUE(config2.Equals(config1));

        // Test |ProxyConfig::proxy_rules.reverse_bypass|.

        config2.proxy_rules().reverse_bypass = true;

        EXPECT_FALSE(config1.Equals(config2));
        EXPECT_FALSE(config2.Equals(config1));

        config1.proxy_rules().reverse_bypass = true;

        EXPECT_TRUE(config1.Equals(config2));
        EXPECT_TRUE(config2.Equals(config1));
    }

    TEST(ProxyConfigTest, ParseProxyRules)
    {
        const struct {
            const char* proxy_rules;

            ProxyConfig::ProxyRules::Type type;
            // These will be PAC-stle strings, eg 'PROXY foo.com'
            const char* single_proxy;
            const char* proxy_for_http;
            const char* proxy_for_https;
            const char* proxy_for_ftp;
            const char* fallback_proxy;
        } tests[] = {
            // One HTTP proxy for all schemes.
            {
                "myproxy:80",

                ProxyConfig::ProxyRules::TYPE_SINGLE_PROXY,
                "PROXY myproxy:80",
                NULL,
                NULL,
                NULL,
                NULL,
            },

            // Multiple HTTP proxies for all schemes.
            {
                "myproxy:80,https://myotherproxy",

                ProxyConfig::ProxyRules::TYPE_SINGLE_PROXY,
                "PROXY myproxy:80;HTTPS myotherproxy:443",
                NULL,
                NULL,
                NULL,
                NULL,
            },

            // Only specify a proxy server for "http://" urls.
            {
                "http=myproxy:80",

                ProxyConfig::ProxyRules::TYPE_PROXY_PER_SCHEME,
                NULL,
                "PROXY myproxy:80",
                NULL,
                NULL,
                NULL,
            },

            // Specify an HTTP proxy for "ftp://" and a SOCKS proxy for "https://" urls.
            {
                "ftp=ftp-proxy ; https=socks4://foopy",

                ProxyConfig::ProxyRules::TYPE_PROXY_PER_SCHEME,
                NULL,
                NULL,
                "SOCKS foopy:1080",
                "PROXY ftp-proxy:80",
                NULL,
            },

            // Give a scheme-specific proxy as well as a non-scheme specific.
            // The first entry "foopy" takes precedance marking this list as
            // TYPE_SINGLE_PROXY.
            {
                "foopy ; ftp=ftp-proxy",

                ProxyConfig::ProxyRules::TYPE_SINGLE_PROXY,
                "PROXY foopy:80",
                NULL,
                NULL,
                NULL,
                NULL,
            },

            // Give a scheme-specific proxy as well as a non-scheme specific.
            // The first entry "ftp=ftp-proxy" takes precedance marking this list as
            // TYPE_PROXY_PER_SCHEME.
            {
                "ftp=ftp-proxy ; foopy",

                ProxyConfig::ProxyRules::TYPE_PROXY_PER_SCHEME,
                NULL,
                NULL,
                NULL,
                "PROXY ftp-proxy:80",
                NULL,
            },

            // Include a list of entries for a single scheme.
            {
                "ftp=ftp1,ftp2,ftp3",

                ProxyConfig::ProxyRules::TYPE_PROXY_PER_SCHEME,
                NULL,
                NULL,
                NULL,
                "PROXY ftp1:80;PROXY ftp2:80;PROXY ftp3:80",
                NULL,
            },

            // Include multiple entries for the same scheme -- they accumulate.
            {
                "http=http1,http2; http=http3",

                ProxyConfig::ProxyRules::TYPE_PROXY_PER_SCHEME,
                NULL,
                "PROXY http1:80;PROXY http2:80;PROXY http3:80",
                NULL,
                NULL,
                NULL,
            },

            // Include lists of entries for multiple schemes.
            {
                "ftp=ftp1,ftp2,ftp3 ; http=http1,http2; ",

                ProxyConfig::ProxyRules::TYPE_PROXY_PER_SCHEME,
                NULL,
                "PROXY http1:80;PROXY http2:80",
                NULL,
                "PROXY ftp1:80;PROXY ftp2:80;PROXY ftp3:80",
                NULL,
            },

            // Include non-default proxy schemes.
            {
                "http=https://secure_proxy; ftp=socks4://socks_proxy; https=socks://foo",

                ProxyConfig::ProxyRules::TYPE_PROXY_PER_SCHEME,
                NULL,
                "HTTPS secure_proxy:443",
                "SOCKS5 foo:1080",
                "SOCKS socks_proxy:1080",
                NULL,
            },

            // Only SOCKS proxy present, others being blank.
            {
                "socks=foopy",

                ProxyConfig::ProxyRules::TYPE_PROXY_PER_SCHEME,
                NULL,
                NULL,
                NULL,
                NULL,
                "SOCKS foopy:1080",
            },

            // SOCKS proxy present along with other proxies too
            {
                "http=httpproxy ; https=httpsproxy ; ftp=ftpproxy ; socks=foopy ",

                ProxyConfig::ProxyRules::TYPE_PROXY_PER_SCHEME,
                NULL,
                "PROXY httpproxy:80",
                "PROXY httpsproxy:80",
                "PROXY ftpproxy:80",
                "SOCKS foopy:1080",
            },

            // SOCKS proxy (with modifier) present along with some proxies
            // (FTP being blank)
            {
                "http=httpproxy ; https=httpsproxy ; socks=socks5://foopy ",

                ProxyConfig::ProxyRules::TYPE_PROXY_PER_SCHEME,
                NULL,
                "PROXY httpproxy:80",
                "PROXY httpsproxy:80",
                NULL,
                "SOCKS5 foopy:1080",
            },

            // Include unsupported schemes -- they are discarded.
            {
                "crazy=foopy ; foo=bar ; https=myhttpsproxy",

                ProxyConfig::ProxyRules::TYPE_PROXY_PER_SCHEME,
                NULL,
                NULL,
                "PROXY myhttpsproxy:80",
                NULL,
                NULL,
            },

            // direct:// as first option for a scheme.
            {
                "http=direct://,myhttpproxy; https=direct://",

                ProxyConfig::ProxyRules::TYPE_PROXY_PER_SCHEME,
                NULL,
                "DIRECT;PROXY myhttpproxy:80",
                "DIRECT",
                NULL,
                NULL,
            },

            // direct:// as a second option for a scheme.
            {
                "http=myhttpproxy,direct://",

                ProxyConfig::ProxyRules::TYPE_PROXY_PER_SCHEME,
                NULL,
                "PROXY myhttpproxy:80;DIRECT",
                NULL,
                NULL,
                NULL,
            },

        };

        ProxyConfig config;

        for (size_t i = 0; i < arraysize(tests); ++i) {
            config.proxy_rules().ParseFromString(tests[i].proxy_rules);

            EXPECT_EQ(tests[i].type, config.proxy_rules().type);
            ExpectProxyServerEquals(tests[i].single_proxy,
                config.proxy_rules().single_proxies);
            ExpectProxyServerEquals(tests[i].proxy_for_http,
                config.proxy_rules().proxies_for_http);
            ExpectProxyServerEquals(tests[i].proxy_for_https,
                config.proxy_rules().proxies_for_https);
            ExpectProxyServerEquals(tests[i].proxy_for_ftp,
                config.proxy_rules().proxies_for_ftp);
            ExpectProxyServerEquals(tests[i].fallback_proxy,
                config.proxy_rules().fallback_proxies);
        }
    }

    TEST(ProxyConfigTest, ProxyRulesSetBypassFlag)
    {
        // Test whether the did_bypass_proxy() flag is set in proxy info correctly.
        ProxyConfig::ProxyRules rules;
        ProxyInfo result;

        rules.ParseFromString("http=httpproxy:80");
        rules.bypass_rules.AddRuleFromString(".com");

        rules.Apply(GURL("http://example.com"), &result);
        EXPECT_TRUE(result.is_direct_only());
        EXPECT_TRUE(result.did_bypass_proxy());

        rules.Apply(GURL("http://example.org"), &result);
        EXPECT_FALSE(result.is_direct());
        EXPECT_FALSE(result.did_bypass_proxy());

        // Try with reversed bypass rules.
        rules.reverse_bypass = true;

        rules.Apply(GURL("http://example.org"), &result);
        EXPECT_TRUE(result.is_direct_only());
        EXPECT_TRUE(result.did_bypass_proxy());

        rules.Apply(GURL("http://example.com"), &result);
        EXPECT_FALSE(result.is_direct());
        EXPECT_FALSE(result.did_bypass_proxy());
    }

    static const char kWsUrl[] = "ws://example.com/echo";
    static const char kWssUrl[] = "wss://example.com/echo";

    class ProxyConfigWebSocketTest : public ::testing::Test {
    protected:
        void ParseFromString(const std::string& rules)
        {
            rules_.ParseFromString(rules);
        }
        void Apply(const GURL& gurl) { rules_.Apply(gurl, &info_); }
        std::string ToPacString() const { return info_.ToPacString(); }

        static GURL WsUrl() { return GURL(kWsUrl); }
        static GURL WssUrl() { return GURL(kWssUrl); }

        ProxyConfig::ProxyRules rules_;
        ProxyInfo info_;
    };

    // If a single proxy is set for all protocols, WebSocket uses it.
    TEST_F(ProxyConfigWebSocketTest, UsesProxy)
    {
        ParseFromString("proxy:3128");
        Apply(WsUrl());
        EXPECT_EQ("PROXY proxy:3128", ToPacString());
    }

    // See RFC6455 Section 4.1. item 3, "_Proxy Usage_".
    TEST_F(ProxyConfigWebSocketTest, PrefersSocks)
    {
        ParseFromString(
            "http=proxy:3128 ; https=sslproxy:3128 ; socks=socksproxy:1080");
        Apply(WsUrl());
        EXPECT_EQ("SOCKS socksproxy:1080", ToPacString());
    }

    TEST_F(ProxyConfigWebSocketTest, PrefersHttpsToHttp)
    {
        ParseFromString("http=proxy:3128 ; https=sslproxy:3128");
        Apply(WssUrl());
        EXPECT_EQ("PROXY sslproxy:3128", ToPacString());
    }

    TEST_F(ProxyConfigWebSocketTest, PrefersHttpsEvenForWs)
    {
        ParseFromString("http=proxy:3128 ; https=sslproxy:3128");
        Apply(WsUrl());
        EXPECT_EQ("PROXY sslproxy:3128", ToPacString());
    }

    TEST_F(ProxyConfigWebSocketTest, PrefersHttpToDirect)
    {
        ParseFromString("http=proxy:3128");
        Apply(WssUrl());
        EXPECT_EQ("PROXY proxy:3128", ToPacString());
    }

    TEST_F(ProxyConfigWebSocketTest, IgnoresFtpProxy)
    {
        ParseFromString("ftp=ftpproxy:3128");
        Apply(WssUrl());
        EXPECT_EQ("DIRECT", ToPacString());
    }

    TEST_F(ProxyConfigWebSocketTest, ObeysBypassRules)
    {
        ParseFromString("http=proxy:3128 ; https=sslproxy:3128");
        rules_.bypass_rules.AddRuleFromString(".chromium.org");
        Apply(GURL("wss://codereview.chromium.org/feed"));
        EXPECT_EQ("DIRECT", ToPacString());
    }

    TEST_F(ProxyConfigWebSocketTest, ObeysLocalBypass)
    {
        ParseFromString("http=proxy:3128 ; https=sslproxy:3128");
        rules_.bypass_rules.AddRuleFromString("<local>");
        Apply(GURL("ws://localhost/feed"));
        EXPECT_EQ("DIRECT", ToPacString());
    }

} // namespace
} // namespace net
